Skip to content

Overhaul of merge conflict tests - #414

Open
lukaskubanek wants to merge 23 commits into
pointfreeco:mainfrom
structuredpath:merge-conflict-tests
Open

lukaskubanek wants to merge 23 commits into
pointfreeco:mainfrom
structuredpath:merge-conflict-tests

Conversation

@lukaskubanek

@lukaskubanek lukaskubanek commented Mar 5, 2026 •

Copy link
Copy Markdown
Contributor

Note

The scenarios in this PR rely on MockCloudDatabase populating CKRecord.modificationDate, which landed in #574. Without it the tests exercise different logic than production.

Summary

This PR replaces the existing merge conflict tests in MergeConflictTests.swift with a new test suite that covers a broader set of scenarios across multiple dimensions:

  • different fields / same fields
  • client newer / server newer / equal timestamps
  • conflict on send / conflict on fetch
  • ordering on send: fetch before retry / retry before fetch

The new tests expose three issues in the merge conflict resolution logic. These are temporarily marked with withKnownIssue, as the main goal of this PR is to document the current behavior, rather than immediately fixing the bugs.

Improvements

  • A simpler Post table type (standalone, no parent entity required) instead of Reminder, which depends on RemindersList.
  • Snapshots assert on container.privateCloudDatabase directly, reducing boilerplate compared to the full container, as the shared database isn’t interesting for these tests.
  • Conflict scenarios use absolute timestamps (e.g. t=30, t=60) for client modification instead of relative increments. This is now aligned with how CKRecords are being treated.
  • Test steps are now annotated and ordered to match real-world event sequences. This is most notable with the client and server edits, which used to not be ordered chronologically.
  • Local database state is now checked for convergence with the CloudKit state after each scenario resolves.

Bugs Identified

The test suite revealed the following issues in the implementation:

  1. A client edit is stamped inconsistently after a conflict. On conflict on send (test 2) the field keeps its edit time of t=30 since #574. On conflict on fetch (test 4) it arrives at the server with the record-level time of t=60. Whether the edit time or the merge time is the right one is a design decision, see #583. Test 4 asserts the edit time for now, in line with the existing upstream test.
  2. The client wins same-field conflicts even when the server record has a newer modification timestamp, violating the “last edit wins” strategy as reported in #354. The conflict-on-send cases (tests 7, 9, 16) were fixed by #574. The conflict-on-fetch case (test 14) is fixed by #581.
  3. Converging edits keep the older timestamp. When client and server independently arrive at the same value, including both removing it, the field keeps the server’s older stamp even when the client’s edit is newer, because the engine only restamps fields whose value changed (tests 10 and 17).

Overview

# Test Name Client Edits Server Edits Expected Result Actual Result Correct? Replaces
1 differentFieldsChange_conflictOnSend_clientNewer isPublished @ t=60 title @ t=30 Both merged Both merged Yes serverRecordUpdatedBeforeClientRecord
2 differentFieldsChange_conflictOnSend_serverNewer isPublished @ t=30 title @ t=60 Both merged Both merged Open (#583) clientRecordUpdatedBeforeServerRecord
3 differentFieldsChange_conflictOnFetch_clientNewer isPublished @ t=60 title @ t=30 Both merged Both merged Yes serverAndClientEditDifferentFields
4 differentFieldsChange_conflictOnFetch_serverNewer isPublished @ t=30 title @ t=60 Both merged Both merged Open (#583) New
5 differentNullableFieldsChange_conflictOnFetch_clientNewer priority @ t=60 dueDate @ t=30 Both merged Both merged Yes mergeWithNullableFields
6 sameFieldChange_conflictOnSend_retryBeforeFetch_clientNewer title @ t=60 title @ t=30 Client Client Yes New
7 sameFieldChange_conflictOnSend_retryBeforeFetch_serverNewer title @ t=30 title @ t=60 Server Server Yes (since #574) New
8 sameFieldChange_conflictOnSend_fetchBeforeRetry_clientNewer title @ t=60 title @ t=30 Client Client Yes serverRecordEditedBeforeClientButProcessedAfterClient
9 sameFieldChange_conflictOnSend_fetchBeforeRetry_serverNewer title @ t=30 title @ t=60 Server Server Yes (since #574) New (originally proposed in #353)
10 sameFieldChange_conflictOnSend_equalValues_clientNewer title @ t=60 Same title @ t=30 Same value, title @ t=60 Same value, title @ t=30 No New
11 sameFieldChange_conflictOnSend_equalValues_serverNewer title @ t=30 Same title @ t=60 Same value, title @ t=60 Same value, title @ t=60 Yes New
12 sameFieldChange_conflictOnSend_equalTimestamps title @ t=60 title @ t=60 Client Client Yes New
13 sameFieldChange_conflictOnFetch_clientNewer title @ t=60 title @ t=30 Client Client Yes serverRecordEditedAndProcessedBeforeClient
14 sameFieldChange_conflictOnFetch_serverNewer title @ t=30 title @ t=60 Server Client No serverRecordEditedAfterClientButProcessedBeforeClient (inverts its expectation, the server should win)
15 sameFieldChangeAndRemoval_conflictOnSend_clientNewer Nulls body @ t=60 Changes body @ t=30 Client Client Yes New
16 sameFieldChangeAndRemoval_conflictOnSend_serverNewer Nulls body @ t=30 Changes body @ t=60 Server Server Yes (since #574) New
17 sameFieldRemoval_conflictOnSend_clientNewer Nulls body @ t=60 Nulls body @ t=30 Both nil, body @ t=60 Both nil, body @ t=30 No New
18 sameFieldRemoval_conflictOnSend_serverNewer Nulls body @ t=30 Nulls body @ t=60 Both nil, body @ t=60 Both nil, body @ t=60 Yes New

Next Steps

  • Tests 2 and 4 follow the outcome of #583. Depending on the design decision there, either the implementation changes or the test flips to the merge time.
  • #581 fixes the remaining conflict-on-fetch case of the second issue and closes #354.
  • The third issue needs a separate discussion. Fixing it requires the engine to recognize a client edit even when the server already holds the same value, which calls for an ancestor to compare against. Whether the last-known server record is suitable to play that role is one of the open questions for the conflict resolution work.

lukaskubanek added a commit to structuredpath/sqlite-data that referenced this pull request Mar 26, 2026
Introduce Post table type

Add differentFieldsChange_conflictOnSend_clientNewer test

Add differentFieldsChange_conflictOnSend_serverNewer test

Remove replaced tests

Add differentFieldsChange_conflictOnFetch_clientNewer test

Add differentFieldsChange_conflictOnFetch_serverNewer test

Add convergence assertions for local state

Remove replaced test

Add sameFieldChange_conflictOnSend_retryBeforeFetch_* tests

Add sameFieldChange_conflictOnSend_fetchBeforeRetry_* tests

Add sameFieldChange_conflictOnFetch_clientNewer test

Add sameFieldChange_conflictOnFetch_serverNewer test

Remove replaced tests

Add sameFieldChangeAndRemoval_* tests

Update test for updating nullable fields

Combine local database assertions into a single join query

Add sameFieldChange_conflictOnSend_equalTimestamps test

Add sameFieldRemoval_conflictOnSend_clientNewer test

Add sameFieldRemoval_conflictOnSend_serverNewer test

Improve synced row query asserts
@lukaskubanek
lukaskubanek marked this pull request as ready for review November 11, 2026 13:27
@lukaskubanek

Copy link
Copy Markdown
Contributor Author

@mbrandonw Now that #574 landed and the tests exercise the same logic as production, I was able to submit this PR for review. It’s now rebased onto main and the description has been updated to account for the fix from #574. For background, see the discussion in #573.

@lukaskubanek

Copy link
Copy Markdown
Contributor Author

It turns out that #574 fixed the restamp for the conflict-on-send case only. The conflict-on-fetch case is described in #583 together with a general limitation of storing the in-flight record. I’ve updated the description of this PR accordingly.

lukaskubanek added a commit to structuredpath/sqlite-data that referenced this pull request Nov 12, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

Violation of “last edit wins” conflict resolution strategy

1 participant